module PNEngine
  class Place < ActiveRecord::Base
    include Resource::InstanceMethods
    extend Resource::ClassMethods
    
    belongs_to :petrinet
    
    if Merb::Config[:use_reevaluation] == true
      has_many :tokens, :dependent => :delete_all, :finder_sql =>
        'SELECT * FROM tokens ' +
        'WHERE tokens.place_id = #{id} AND tokens.is_active = true'
    else
      has_many :tokens, :dependent => :delete_all
    end
    has_many :locators, :dependent => :delete_all
    has_many :arcs
    
    # Deletes all tokens with given case from place.
    def reset_tokens(c)
      self.tokens.find(:all, :conditions => ["case_id = ?", c.id]).collect{|t|t.destroy}
    end
    
    # Puts new token on place
    def push(token)
      #Merb.logger.info("#{self.tokens.size}")
      self.tokens<< token
      self.save!
      self.reload
      #Merb.logger.info("#{self.tokens.size}")
    end
    
    # Consumes given token from place
    def pull(token)
      self.tokens.destroy(token.id)
      self.save!
      self.reload
    end
    
    # Produces Atom feed representation for place 
    def to_atom
      feed = Atom::Feed.new
      feed.title = name
      tokens.each do |token|
        feed << token.to_atom
      end
      feed
    end
		
    # Produces pnml representation for place
    def to_pnml(as_document = false)
      #TODO: only add token to pnml if it's not a pullplace with read-only set

      pnml_doc = REXML::Document.new
      pplace = pnml_doc.add_element('place', {'id' => pid})
			
      #add name if specified
      if name
        pname = pplace.add_element('name')
        ptext = pname.add_element('text')
        ptext.text = name
      end

      if feed_url || locators || tokens
        toolspecific = pplace.add_element('toolspecific', {'tool' => 'Petri Net Engine', 'version' => '1.0'})
				
        #add external
        toolspecific.add_element('external', {'type' => 'pull', 'href' => feed_url}) if feed_url
				
        #add locators
        if locators
          locators.each do |locator|
            plocator = toolspecific.add_element('locator')
            loc_name = plocator.add_element('name')
            loc_name.text = locator.name

            loc_type = plocator.add_element('type')
            loc_type.text = locator.val_type

            loc_expr = plocator.add_element('expr')
            loc_expr.text = locator.expr
          end
        end

        #add token references
        if tokens
          tokens.each do |token|
            toolspecific.add_element('token', {'href' => token.url})
          end
        end
      end

      output = ''
      as_document ? pnml_doc : pnml_doc.write(output, 2, true)
    end
    
    private
    before_update :sanitize
    #strips place name
    def sanitize
      self.name = self.name.strip
    end
    
    before_create :label_place
    def label_place
      self.name = self.pid if self.name.blank?
    end

  end
  
  class ExternalPlace < Place
    # each Push- or PullPlace should have an url as String 
    def before_save
      self.feed_url = self.feed_url.to_s if self.feed_url && self.feed_url.is_a?(URI::HTTP)
      true
    end
  end
  
  class PushPlace < ExternalPlace
    # Overwrites push method of Place
    def push(token)
      raise "A PushPlace must have a feed_url before pushing!" if feed_url.nil?
      AtomReq.post!(feed_url, token.to_atom)
      token.destroy
      self.reload
    end
    
    # Overwrites pull method of Place
    def pull(token)
      raise "From PushPlace it can't be consumed tokens"
    end
  end
  
  class BeamPlace < PushPlace
    before_create :create_beam_locator
    
    # Overwrites push method of PushPlace
    def push(token)
      beam_url = token.values["beamTo"]
      raise "A token on BeamPlace must have a beamTo before pushing!" if beam_url.nil?
      AtomReq.post!(beam_url, token.to_atom)
      token.destroy
      self.reload
    end
    
    private
    def create_beam_locator
      self.locators.create!(:name => "beamTo", :expr => "//@beamTo", :val_type => "xsd:anyURI")
    end
  end
  
  class PullPlace < ExternalPlace
    serialize :consumed_tokens, Array
    
    after_create :sync
    before_update :eval_for_conditional_sync
    after_update :conditional_sync
    before_create :init_consumed_tokens

    def after_find
      init_consumed_tokens
    end
    
    # Fetches external feed and updates database
    def sync
      #Step 1: Eval, if newer feed version is available
      begin
        header = AtomReq.head!(self.feed_url).to_hash
      rescue SocketError
        Merb.logger.error("#{feed_url} not reachable.")
        return nil
      end
      if self.etag && header["etag"]
        return if self.etag == header["etag"]
      elsif self.last_modified && DateTime.parse(header["last-modified"])
        return if self.last_modified == DateTime.parse(header["last-modified"])
      end
      
      #Step 2: Set new modified flags
      self.last_modified = DateTime.parse(header["last-modified"]) rescue nil
      self.etag = header["etag"]
      
      #Step 3: Get updated Feed
      begin
        feed = Atom::Collection.new(feed_url)
        feed.update!
      rescue SocketError
        Merb.logger.error("#{feed_url} not reachable.")
        return nil
      end
      
      #Step 4: Update place
      name = feed.title
      
      local_uuids = tokens.collect{|token| token.uuid}
      remote_uuids = feed.entries.collect{|entry| entry.id}

      new_uuids = remote_uuids - local_uuids - consumed_tokens
      
      feed.entries.select{|entry| new_uuids.include?(entry.id)}.each do |entry|
        self.tokens.create!(:value => entry.to_s, :uuid => entry.id)
      end
            
      existing_uuids = remote_uuids & local_uuids
      #TODO existing tokens should be updated
      
      deleted_uuids = local_uuids - remote_uuids
      tokens.select{|token| deleted_uuids.include?(token.uuid)}.each do |token|
        token.destroy
      end
      
      reload
    end
    
    # Overwrites push-method of Place
    def push(token)
      raise "PullPlace can't get new tokens"
    end
    
    # Overwrites pull-method of Place
    def pull(token)
      raise ArgumentError, "Token should not be nil" if token.nil?
      self.consumed_tokens << token.uuid
      token.destroy
      self.save!
      self.reload
    end
    
    private
    def conditional_sync
      if @feed_url_updated
        self.determine_modified_headers
        self.sync
      end
      true
    end
    
    #sets @feed_url_updated to true if a) feed_url is set in new record or b) feed_url has been updated
    def eval_for_conditional_sync
      @feed_url_updated = !(Place.find(id).feed_url == self.feed_url)
      true
    end
    
    def init_consumed_tokens
      self.consumed_tokens = []
      true
    end
  end
end
